﻿// ReSharper disable once CheckNamespace
namespace Fluent;

using System;
using System.Windows;
using System.Windows.Controls;
using Fluent.Internal;
using Fluent.Internal.KnownBoxes;

/// <summary>
/// UniformGrid is used to arrange children in a grid with all equal cell sizes.
/// </summary>
public class UniformGridWithItemSize : Panel
{
    private int rows;
    private int columns;
    private int nonCollapsedCount;
    private int usedColumns;

    /// <summary>
    /// Gets or sets panel orientation
    /// </summary>
    public Orientation Orientation
    {
        get { return (Orientation)this.GetValue(OrientationProperty); }
        set { this.SetValue(OrientationProperty, value); }
    }

    /// <summary>Identifies the <see cref="Orientation"/> dependency property.</summary>
    public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register(
            nameof(Orientation), 
            typeof(Orientation),
            typeof(UniformGridWithItemSize), 
            new FrameworkPropertyMetadata(Orientation.Horizontal,
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure));

    /// <summary>
    /// Specifies the number of maximum columns in the grid
    /// </summary>
    public int MinColumns
    {
        get => this.Orientation == Orientation.Horizontal ? (int)this.GetValue(MinColumnsProperty) : 1;
        set => this.SetValue(MinColumnsProperty, value);
    }

    /// <summary>Identifies the <see cref="MinColumns"/> dependency property.</summary>
    public static readonly DependencyProperty MinColumnsProperty =
        DependencyProperty.Register(
            nameof(MinColumns),
            typeof(int),
            typeof(UniformGridWithItemSize),
            new FrameworkPropertyMetadata(IntBoxes.Zero,
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure),
            ValidateMinColumns);

    private static bool ValidateMinColumns(object? o)
    {
        return o is int intValue && intValue >= 0;
    }

    /// <summary>
    /// Specifies the number of maximum columns in the grid
    /// </summary>
    public int MaxColumns
    {
        get => this.Orientation == Orientation.Horizontal ? (int)this.GetValue(MaxColumnsProperty) : 1;
        set => this.SetValue(MaxColumnsProperty, value);
    }

    /// <summary>Identifies the <see cref="MaxColumns"/> dependency property.</summary>
    public static readonly DependencyProperty MaxColumnsProperty =
        DependencyProperty.Register(
            nameof(MaxColumns),
            typeof(int),
            typeof(UniformGridWithItemSize),
            new FrameworkPropertyMetadata(IntBoxes.Zero,
                FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure),
            ValidateMaxColumns);

    private static bool ValidateMaxColumns(object? o)
    {
        return o is int intValue && intValue >= 0;
    }

    /// <summary>Identifies the <see cref="ItemWidth"/> dependency property.</summary>
    public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register(
        nameof(ItemWidth), typeof(double), typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata(DoubleBoxes.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure));

    /// <summary>
    /// Specifies the item width.
    /// </summary>
    public double ItemWidth
    {
        get => (double)this.GetValue(ItemWidthProperty);
        set => this.SetValue(ItemWidthProperty, value);
    }

    /// <summary>Identifies the <see cref="ItemHeight"/> dependency property.</summary>
    public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register(
        nameof(ItemHeight), typeof(double), typeof(UniformGridWithItemSize), new FrameworkPropertyMetadata(DoubleBoxes.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsParentMeasure));

    /// <summary>
    /// Specifies the item height.
    /// </summary>
    public double ItemHeight
    {
        get => (double)this.GetValue(ItemHeightProperty);
        set => this.SetValue(ItemHeightProperty, value);
    }

    /// <summary>
    /// Compute the desired size of this UniformGrid by measuring all of the
    /// children with a constraint equal to a cell's portion of the given
    /// constraint (e.g. for a 2 x 4 grid, the child constraint would be
    /// constraint.Width*0.5 x constraint.Height*0.25).  The maximum child
    /// width and maximum child height are tracked, and then the desired size
    /// is computed by multiplying these maximums by the row and column count
    /// (e.g. for a 2 x 4 grid, the desired size for the UniformGrid would be
    /// maxChildDesiredWidth*2 x maxChildDesiredHeight*4).
    /// </summary>
    /// <param name="constraint">Constraint</param>
    /// <returns>Desired size</returns>
    protected override Size MeasureOverride(Size constraint)
    {
        this.UpdateComputedValues(this.MaxColumns);

        var useDefinedItemWidth = double.IsNaN(this.ItemWidth) == false && DoubleUtil.AreClose(this.ItemWidth, 0) == false;
        var maxItemWidth = useDefinedItemWidth
            ? this.ItemWidth
            : constraint.Width / this.columns;
        var useDefinedItemHeight = double.IsNaN(this.ItemHeight) == false && DoubleUtil.AreClose(this.ItemHeight, 0) == false;
        var maxItemHeight = useDefinedItemHeight
            ? this.ItemHeight
            : constraint.Height / this.rows;

        var childConstraint = new Size(maxItemWidth, maxItemHeight);
        var maxChildDesiredWidth = 0.0;
        var maxChildDesiredHeight = 0.0;

        //  Measure each child, keeping track of maximum desired width and height.
        for (int i = 0, count = this.InternalChildren.Count; i < count; ++i)
        {
            var child = this.InternalChildren[i];

            // Measure the child.
            child.Measure(childConstraint);
            var childDesiredSize = child.DesiredSize;

            if (useDefinedItemWidth)
            {
                maxChildDesiredWidth = this.ItemWidth;
            }
            else
            {
                if (maxChildDesiredWidth < childDesiredSize.Width)
                {
                    maxChildDesiredWidth = childDesiredSize.Width;
                }
            }

            if (useDefinedItemHeight)
            {
                maxChildDesiredHeight = this.ItemHeight;
            }
            else
            {
                if (maxChildDesiredHeight < childDesiredSize.Height)
                {
                    maxChildDesiredHeight = childDesiredSize.Height;
                }
            }
        }

        if (double.IsNaN(constraint.Width) == false
            && double.IsPositiveInfinity(constraint.Width) == false)
        {
            var precomputedColumns = this.MaxColumns != 0
                ? Math.Min(this.MaxColumns, (int)(constraint.Width / maxChildDesiredWidth))
                : (int)(constraint.Width / maxChildDesiredWidth);
            this.UpdateComputedValues(precomputedColumns);
        }

        return new Size(maxChildDesiredWidth * this.usedColumns, maxChildDesiredHeight * this.rows);
    }

    /// <summary>
    /// Arrange the children of this UniformGrid by distributing space evenly 
    /// among all of the children, making each child the size equal to a cell's
    /// portion of the given arrangeSize (e.g. for a 2 x 4 grid, the child size
    /// would be arrangeSize*0.5 x arrangeSize*0.25)
    /// </summary>
    /// <param name="arrangeSize">Arrange size</param>
    protected override Size ArrangeOverride(Size arrangeSize)
    {
        var childBounds = new Rect(0, 0, arrangeSize.Width / this.usedColumns, arrangeSize.Height / this.rows);
        var xStep = childBounds.Width;
        var xBound = arrangeSize.Width;

        // Arrange and Position each child to the same cell size
        foreach (UIElement? child in this.InternalChildren)
        {
            if (child is null)
            {
                continue;
            }

            child.Arrange(childBounds);

            // only advance to the next grid cell if the child was not collapsed
            if (child.Visibility != Visibility.Collapsed)
            {
                childBounds.X += xStep;

                if (DoubleUtil.GreaterThan(childBounds.X, xBound) || DoubleUtil.AreClose(childBounds.X, xBound))
                {
                    childBounds.Y += childBounds.Height;
                    childBounds.X = 0;
                }
            }
        }

        return arrangeSize;
    }

    /// <summary>
    /// If either Rows or Columns are set to 0, then dynamically compute these
    /// values based on the actual number of non-collapsed children.
    ///
    /// In the case when both Rows and Columns are set to 0, then make Rows 
    /// and Columns be equal, thus laying out in a square grid.
    /// </summary>
    private void UpdateComputedValues(int precomputedColumns = 0)
    {
        this.columns = precomputedColumns;
        this.rows = 0;

        this.nonCollapsedCount = 0;

        // First compute the actual # of non-collapsed children to be laid out
        for (int i = 0, count = this.InternalChildren.Count; i < count; ++i)
        {
            var child = this.InternalChildren[i];
            if (child.Visibility != Visibility.Collapsed)
            {
                this.nonCollapsedCount++;
            }
        }

        // to ensure that we have at leat one row & column, make sure
        // that nonCollapsedCount is at least 1
        if (this.nonCollapsedCount == 0)
        {
            this.nonCollapsedCount = 1;
        }

        if (this.columns == 0)
        {
            // columns are unset -- lay out in a square
            this.columns = (int)Math.Sqrt(this.nonCollapsedCount);
            if (this.columns * this.columns < this.nonCollapsedCount)
            {
                this.columns++;
            }
        }

        if (this.MinColumns != 0
            && this.columns < this.MinColumns)
        {
            this.columns = this.MinColumns;
        }

        if (this.MaxColumns != 0
            && this.columns > this.MaxColumns)
        {
            this.columns = this.MaxColumns;
        }

        this.columns = Math.Max(1, this.columns);

        // -1 because we ensured that nonCollapsedCount is at least 1
        this.rows = (this.nonCollapsedCount + (this.columns - 1)) / this.columns;
        this.rows = Math.Max(1, this.rows);

        this.usedColumns = Math.Min(this.columns, this.nonCollapsedCount);
    }
}